﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Diagnostics;
using OverStore.Runtime.Logger;
using System.Globalization;

namespace OverStore.Runtime.Storage
{
    /// <summary>
    /// Executes single SQL instruction using specified parameters.
    /// </summary>
    public class AdoNetCommand : AdoNetCommandBase
    {
        private const string LogCategory = "ADONETCOMMAND";
        private readonly List<AdoNetParameter> Parameters;
        private readonly CommandType CommandType;
        private readonly AdoCommandResultType ResultGettingType;
        private readonly string Sql;
        private readonly ILogger Logger;

        /// <summary>
        /// Initializes a new instance of the <see cref="AdoNetCommand"/> class.
        /// </summary>
        /// <param name="sql">The SQL query or Stored Procedure name.</param>
        /// <param name="commandType">OverrideDbType of the command: text, or stored procedure.</param>
        /// <param name="parameters">The collection of command parameters.</param>
        /// <param name="resultGettingType">Type of the result getting.</param>
        /// <param name="logger">The logger.</param>
        /// <exception cref="ArgumentNullException"><paramref name="parameters"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="logger"/> is null reference.</exception>
        public AdoNetCommand(string sql, CommandType commandType, IEnumerable<AdoNetParameter> parameters, AdoCommandResultType resultGettingType, ILogger logger)
        {
            if (Object.ReferenceEquals(parameters, null))
                throw new ArgumentNullException("parameters");
            Parameters = parameters.ToList();
            CommandType = commandType;
            if (String.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql");
            Sql = sql;
            ResultGettingType = resultGettingType;
            Logger = logger;
        }

        /// <summary>
        /// If <paramref name="result"/> is <see cref="DBNull.Value"/>, returns <c>null</c>, otherwise returns result.
        /// </summary>
        private static object OmitDbNulls(object value)
        {
            return DBNull.Value.Equals(value) ? null : value;
        }

        /// <summary>
        /// Returns the first row from opened reader. If reader contains no rows, method returns <see cref="EmptyTypedRecord.Value"/>.
        /// </summary>
        private static TypedRecord ReadFirst(IDataReader reader, out int rowsAffected)
        {
            Debug.Assert(reader != null, "Read First from null reader.");
            rowsAffected = 0;
            if (reader.Read())
            {
                rowsAffected = 1;
                return new DictionaryTypedRecord(
                    Enumerable.Range(0, reader.FieldCount).ToDictionary(
                            idx => reader.GetName(idx),
                            idx => OmitDbNulls(reader.GetValue(idx)),
                            StringComparer.OrdinalIgnoreCase));
            }
            else
                return EmptyTypedRecord.Value;
        }

        /// <summary>
        /// Returns current values of specified command parameters as instance of <see cref="TypedRecord"/> type.
        /// </summary>
        private static TypedRecord RecordFromParameters(IDbCommand command)
        {
            Debug.Assert(command != null, "Command is null.");
            return new DictionaryTypedRecord(
                    command.Parameters.Cast<IDbDataParameter>()
                           .ToDictionary(p => p.ParameterName, p => OmitDbNulls(p.Value), StringComparer.OrdinalIgnoreCase));
        }

        private static string ToParametersString(IDbCommand command)
        {
            var parametersNameValueStrings = command.Parameters
                                                    .Cast<IDataParameter>()
                                                    .Select(p => String.Format(CultureInfo.InvariantCulture, "{0} = {1}", p.ParameterName, p.Value))
                                                    .ToArray();
            return string.Join(", ", parametersNameValueStrings);
        }

        private void LogDbCommand(string action, IDbCommand command)
        {
            if (Logger != null)
            {
                Func<string> getLogString =
                    () =>
                    String.Format(CultureInfo.InvariantCulture, "COMMAND: {0}\r\nPARAMETERS: {1}", command.CommandText,
                                  ToParametersString(command));
                Logger.Log(this, LogCategory, action);
                Logger.Log(this, LogCategory, getLogString);
            }
        }

        /// <summary>
        /// Initialize specified <see cref="IDbCommand"/> with set of parameters based on collection of
        /// <see cref="AdoNetParameter"/> objects.
        /// </summary>
        private IDbCommand FillDbCommandWithParameters(IDbCommand dbCommand, IRecord parameters, bool isCommandReused)
        {
            if (dbCommand == null)
                throw new ArgumentNullException("dbCommand");
            if (parameters == null)
                throw new ArgumentNullException("parameters");
            if (!isCommandReused)
            {
                foreach (var p in Parameters)
                {
                    var dbParameter = dbCommand.CreateParameter();
                    dbParameter.ParameterName = p.Name;
                    dbParameter.DbType = p.DataType;
                    dbParameter.Direction = p.Direction;
                    dbCommand.Parameters.Add(dbParameter);
                }
            }
            object[] values = new object[Parameters.Count];
            for (int i = 0; i < Parameters.Count; i++)
            {
                var p = Parameters[i];
                object paramValue;
                if (!parameters.TryGetValue(p.Name, out paramValue))
                    throw new PersistingRuntimeException(
                        p.ErrorMessage ?? String.Format(
                                              CultureInfo.InvariantCulture,
                                              "Ado.Net Command Error: parameter with name {0} is missing.",
                                              p.Name));
                if (paramValue == null && !p.AllowNull)
                    throw new PersistingRuntimeException(
                        p.ErrorMessage ?? String.Format(
                                              CultureInfo.InvariantCulture,
                                              "Ado.Net Command Error: parameter with name {0} does not allow null but null value passed.",
                                              p.Name));
                values[i] = paramValue ?? DBNull.Value;
            }
            for (int i = 0; i < values.Length; i++)
            {
                var dbParam = dbCommand.Parameters[i] as IDataParameter;
                dbParam.Value = values[i];
            }
            
            return dbCommand;
        }

        /// <summary>
        /// Executes current command using specified <see cref="IDbCommand"/> and read all data from resultset returned.
        /// </summary>
        public override IEnumerable<TypedRecord> ReadAll(Func<AdoNetCommandBase, DataCommandContainer> createCommand, IRecord parameters)
        {
            if (createCommand == null)
                throw new ArgumentNullException("createCommand");
            if (parameters == null)
                throw new ArgumentNullException("parameters");

            return new ReadResult(() =>
            {
                var container = createCommand(this);
                var command = FillDbCommandWithParameters(container.Command, parameters, container.IsCommandReused);
                if (!container.IsCommandReused)
                {
                    command.CommandType = CommandType;
                    command.CommandText = Sql;
                }
                LogDbCommand("OPEN READER", command);
                return container;
            });
        }

        /// <summary>
        /// Executes specified command and return command result.
        /// </summary>
        public override TypedRecord Execute(Func<AdoNetCommandBase, DataCommandContainer> createCommand, IRecord parameters, out int rowsAffected)
        {
            if (createCommand == null)
                throw new ArgumentNullException("createCommand");
            if (parameters == null)
                throw new ArgumentNullException("parameters");
            try
            {
                using (var container = createCommand(this))
                {
                    var command = FillDbCommandWithParameters(container.Command, parameters, container.IsCommandReused);
                    if (!container.IsCommandReused || String.IsNullOrEmpty(command.CommandText))
                    {
                        command.CommandText = Sql;
                        command.CommandType = CommandType;
                    }
                    LogDbCommand("EXECUTE", command);
                    if (ResultGettingType == AdoCommandResultType.DataReaderResultSet)
                    {
                        using (var dbReader = command.ExecuteReader())
                            return ReadFirst(dbReader, out rowsAffected);
                    }

                    if (ResultGettingType == AdoCommandResultType.MergePreferResultSet ||
                        ResultGettingType == AdoCommandResultType.MergePreferOutputParameters)
                    {
                        TypedRecord resultset = null;
                        using (var dbReader = command.ExecuteReader())
                        {
                            resultset = ReadFirst(dbReader, out rowsAffected);
                        }

                        switch (ResultGettingType)
                        {
                            case AdoCommandResultType.MergePreferResultSet:
                                return new CompositeRecord(resultset, RecordFromParameters(command));
                            case AdoCommandResultType.MergePreferOutputParameters:
                                return new CompositeRecord(RecordFromParameters(command), resultset);
                            default:
                                throw new NotSupportedException("Error in result getting type.");
                        }
                    }

                    if (ResultGettingType == AdoCommandResultType.OutputParameters)
                    {
                        rowsAffected = command.ExecuteNonQuery();
                        return RecordFromParameters(command);
                    }

                    throw new NotSupportedException("Unknown AdoCommandResultType.");
                }
            }
            catch (PersistingRuntimeException)
            {
                throw;
            }
            catch (Exception ex)
            {
                if (Logger != null)
                    Logger.Log(
                        this,
                        LogCategory,
                        () => String.Format("Command execution causes exception of type {0} with message {1}\r\n", ex.GetType(), ex.Message) + ex);
                throw new PersistingRuntimeException(ex.Message, ex);
            }
        }
    }
}
